package com.wzfckj.common.security.config;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.oauth2.OAuth2ClientProperties;
import org.springframework.boot.autoconfigure.security.oauth2.resource.ResourceServerProperties;
import org.springframework.boot.autoconfigure.security.oauth2.resource.SpringSocialTokenServices;
import org.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoTokenServices;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.*;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.RestTemplate;

/**
 * oauth2 服务配置 (处理资源服务器的Token)
 * 这里注入oauth2相关的config
 *
 * 为什么认证服务器也需要变成资源服务器：
 * 因为oauth2在授权码模式下 oauth客户端在在获取access_token后 会从后台调用授权服务器的用户信息端点来构建认证对象（Authentication）
 * 因为用户信息端点需要被资源服务器保护起来 所以需要将授权服务器同时配置为资源服务器
 *
 * 另外
 *
 * 资源服务器的职责是对来自oauth客户端的access_token进行鉴权 一个资源服务器包含多个端点（接口） 一部分端点作为资源服务器的资源
 * 提供给oauth的client访问 另一部分端点不由资源服务器管理 由资源服务器管理的端点安全性配置在此类中
 * 其余端点安全性配置在SecurityConfiguration类中
 * 当请求中包含OAuth2 access_token时，Spring Security会根据资源服务器配置进行过滤
 * EnableResourceServer会创建一个WebSecurityConfigurerAdapter 执行顺序是 3
 * 在SecurityConfiguration类之前执行 优先级更高
 *
 * 用户携带令牌去访问网关 -> 网关转发到 -> 资源服务器 ->资源服务器携带token去访问认证服务器
 * -> 认证服务器调用tokenstore校验token合法性 -> 认证服务器返回资源服务器当前用户的身份和权限等
 *
 * @author ruoyi
 */
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    private Logger logger = LoggerFactory.getLogger(ResourceServerConfig.class);
    @Autowired
    private ResourceServerProperties resourceServerProperties;

    @Autowired
    private OAuth2ClientProperties oAuth2ClientProperties;

    @Bean
    public AuthIgnoreConfig authIgnoreConfig() {
        return new AuthIgnoreConfig();
    }

    /**
     * 这里加入restTemplate的bean是因为 配置文件中 security的配置的uri都是用服务名配置的 如果不配置找不到host
     * 具体实现参照
     *
     * @return restTemplate
     * @link https://www.jianshu.com/p/79d63e374947
     */
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        restTemplate.setErrorHandler(new DefaultResponseErrorHandler());
        return restTemplate;
    }

    /**
     * {@link ResourceServerTokenServices} oauth2资源服务器鉴权的一个核心服务接口类，只有2个接口方法
     * {@link DefaultTokenServices}既实现了ResourceServerTokenServices(缩写)，又实现了AuthorizationServerTokenServices，
     * 并且内部实现是对tokenStore的操作该类适用于认证服务器的token管理 生成token 刷新token 获取token
     * {@link RemoteTokenServices} 检查token的服务，对应配置security.oauth2.resource.tokenInfoUrl=xxx/oauth/check_token
     * {@link SpringSocialTokenServices} 封装了对第三方统一授权的调用方法，比如集成QQ,微信,微博
     * {@link UserInfoTokenServices} 获取鉴权用户信息的服务,对应的配置文件中security.oauth2.resource.user-info-uri=xxx/auth/current
     *
     * @return remoteTokenServices
     * @link https://www.jianshu.com/p/79d63e374947
     */
    @Bean
    public ResourceServerTokenServices tokenServices() {
        //是用于向远程认证服务器验证token,同时获取token对应的用户的信息
        //对RemoteTokenServices重写 防止无法识别的host
        RemoteTokenServices remoteTokenServices = new RemoteTokenServices();
        //是我们用来生成token的转换器
        DefaultAccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter();
        //来对应处理token与userinfo的获取、转换 这里使用了pig项目lengleng写的自我实现
        UserAuthenticationConverter userTokenConverter = new CommonUserConverter();

        accessTokenConverter.setUserTokenConverter(userTokenConverter);
        remoteTokenServices.setCheckTokenEndpointUrl(resourceServerProperties.getTokenInfoUri());

        remoteTokenServices.setClientId(oAuth2ClientProperties.getClientId());
        remoteTokenServices.setClientSecret(oAuth2ClientProperties.getClientSecret());
        remoteTokenServices.setRestTemplate(restTemplate());
        remoteTokenServices.setAccessTokenConverter(accessTokenConverter);
        return remoteTokenServices;
    }

    /**
     * 注意:这里配置的URL与过滤器的映射关系的优先级大于SecurityConfiguration类
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {

        logger.info("ResourceServer配置执行");

        http.csrf().disable();
        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http
                .authorizeRequests();
//        .antMatchers("/aaa/**").access("#oauth2.hasScope('server2')")拓展
        // 不登录可以访问 --> 白名单 对象application.properties里的配置
        authIgnoreConfig().getUrls().forEach(url -> registry.antMatchers(url).permitAll());
        registry.anyRequest().authenticated();
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.tokenServices(tokenServices());
    }
}
